package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.ScrollResult;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.Follow;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IFollowService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.LongValue;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService;

    @Resource
    private IFollowService followService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Result qureyBlog(Long id) {
        log.info("前端传来的bolg的id为{}", id);
        Blog byId = this.getById(id);
        if (byId == null) {
            return Result.fail("笔记不存在！");
        }
        //展示blog的时候还要展示发bolg的用户，所以要查一下用户的图标和昵称
        Long userId = byId.getUserId();
        User user = userService.getById(userId);
        byId.setIcon(user.getIcon());
        byId.setName(user.getNickName());
        //实现点赞之后，会出现出现点赞高亮效果
        Long id1 = UserHolder.getUser().getId();
        if (id1 != null) {
            boolean isLike = islike(id, id1);
            byId.setIsLike(isLike);
        }
        return Result.ok(byId);
    }

    @Override
    public Result queryHotBlog(Integer current) {
        // 根据用户查询
        Page<Blog> page = this.query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog -> {
            Long userId = blog.getUserId();
            User user = userService.getById(userId);
            blog.setName(user.getNickName());
            blog.setIcon(user.getIcon());
            //实现当前用户点赞后，会出现点赞高亮
            UserDTO user1 = UserHolder.getUser();
            if (user1 != null) {
                boolean isLike = islike(blog.getId(), user1.getId());
                blog.setIsLike(isLike);
            }
        });
        return Result.ok(records);
    }

    @Override
    public Result showUserBlog(Long id, Integer current) {
        Page<Blog> page = new Page();
        page.setCurrent(current);
        page.setSize(SystemConstants.MAX_PAGE_SIZE);
        Page<Blog> page1 = this.query().page(page);
        List<Blog> blogs = page1.getRecords();
        if (blogs.isEmpty()) {
            return Result.fail("当前用户还没有发布过blog!");
        }
        return Result.ok(blogs);
    }

    @Override
    @Transactional
    public Result saveBlog(Blog blog) {
        //在这里实现对新发布的blog进行保存，同时使用feed流的推模式，结合redis的zSet作为粉丝的收件箱，推送给粉丝
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            return Result.fail("请先完成登录！");
        }
        blog.setUserId(user.getId());
        // 保存探店博文
        save(blog);
        // 推送blog给自己的粉丝
        LambdaQueryWrapper<Follow> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Follow::getFollowUserId, user.getId());
        List<Follow> fans = followService.list(wrapper);
        fans.stream().map(item -> {
            //取出粉丝id，并以前缀+粉丝id为key，新发布的blogId为value，score为当前时间戳存入redis中，用zSet的数据类型
            String userId = item.getUserId().toString();
            String key = "newBlog:" + userId;
            //前端传来的blog信息封装时没有id，但是save(blog);之后，id会从数据库中写回到我们的封装对象里，所以我可以blog.getId().toString()
            //System.currentTimeMillis() 返回值是long，是一个时间戳
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
            return null;
        }).collect(Collectors.toList());
        return Result.ok(blog.getId());
    }

    @Override
    public Result followPush(Long lastId, Integer offset) {
        //1.从收件箱中取出blog的信息（blogId）
        UserDTO user = UserHolder.getUser();
        if (user == null) {
            return Result.fail("请先完成登录！");
        }
        String userId = user.getId().toString();
        String key = "newBlog:" + userId;
        /*
         * 从redis中查询，且待条件的返回：
         * 调用 reverseRangeByScoreWithScores（） 按照score的大小从大到小降序排列，恰好复合我们的业务
         * 时间戳大的即发布时间在后的，显示在前面。
         * 同时，该方法我们在这要传的形参有4个，  一个是key   ，还有就是范围： max 从那个score开始， min 到那个 score结束，
         * 以及 limit 中的 offset（偏移量）和 count（要查询的数量）
         * 我们在当前业务中只关注最新，则min的值可以写死，写0，表示时间的最小值 ，同时count 我们也可以写死成某个值
         * */

        //todo:完成对redis的当前用户的收件箱查询，并返回blogid组成的list 前端会传来 max 和 offset
        //形参顺序分别是（key,min,max,offset,count）
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate
                .opsForZSet().reverseRangeByScoreWithScores(key, 0, lastId, offset, 3);

        if (typedTuples.isEmpty() || typedTuples == null) {
            //到这表示没有新的blog
            return Result.ok();
        }
        //对返回的set集合进行解析  blogId max offset
        List<Long> blogIds = new ArrayList<>(typedTuples.size());
        //minTime是可以写死的  offset除了第一次为0，其他的时候要看情况分析
        long minTime = 0;

        //确定一下offset，那么就有可能就是score相同的情况，至少有一个那就是它本身，所以初始值为1
        int os = 1;
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
            //在这每一个tuple中存着redis中zSet的每一个成员及其score， 调用getValue（）--->获取value，调用 getScore（）---->获取score
            Long blogId = Long.valueOf(tuple.getValue());
            blogIds.add(blogId);
            //获取当前成员的分数（时间戳）
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                //就是出现了分数一样的情况，那么offset就要+1
                os++;
            } else {
                //走到这里，表示当前取到的成员的score和minTime不相同，越往后取，时间戳就会越小（ typedTuples在从redis中取出来的时候以及排好序了）
                minTime = time;
                //os重置，以防下一次和minTime相同的出现
                os = 1;
            }
        }
        //todo:对blogId的List进行解析，从数据库中获取到这些blog信息，排好序，score（发布时间戳）越大越在前显示，并封装成List返回给前端
        //1.把解析好的 BlogIds 集合去数据库查，按顺序查出来
        String idStr = StrUtil.join(",", blogIds); //把blogId 拼成字符串
        List<Blog> blogs = query().in("id", blogIds).last("order by field(id," + idStr + ")").list();

        //对blogs中的blog进行是否点赞处理和显示当前blog的发布用户信息
        for (Blog blog : blogs) {
            //获取当前blog的发布用户信息
            Long userId1 = blog.getUserId();
            User byId = userService.getById(userId1);
            blog.setName(byId.getNickName());
            blog.setIcon(byId.getIcon());
            //验证当前用户是否对blog点赞
            Long id1 = UserHolder.getUser().getId();
            if (id1 != null) {
                boolean isLike = islike(blog.getId(), userId1);
                blog.setIsLike(isLike);
            }
        }

        //将 blogs的list集合 和 offset 以及 minTime 封装好发给前端
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setMinTime(minTime);
        scrollResult.setOffset(os);
        return Result.ok(scrollResult);
    }


    private boolean islike(Long blogId, Long userId) {
        //todo:判断当前用户是否对blog点赞
        Double isMember = stringRedisTemplate.opsForZSet()
                .score("like:blog:" + String.valueOf(blogId), String.valueOf(userId));
        if (isMember != null) {
            //在这个blog的点赞池中，当前用户已存在，则表示已点过赞了
            return true;
        }
        //查不到，表示没点过赞
        return false;
    }

    @Override
    public Result likesFunction(Long id) {
        //修改一下点赞功能，就是点赞一次，即为点赞，点赞了之后再次点击点赞按钮，则会取消点赞
        //再修改一下，之前用的set集合，是无序的，现在用有效的zSet来实现点赞排行榜功能
        Double isMember = stringRedisTemplate.opsForZSet()
                .score("like:blog:" + String.valueOf(id), String.valueOf(UserHolder.getUser().getId()));

        if (isMember != null) {
            //已经点过赞了，现在再点击，就是取消点赞
            //1.对redis中操作
            stringRedisTemplate.opsForZSet()
                    .remove("like:blog:" + String.valueOf(id), String.valueOf(UserHolder.getUser().getId()));
            //2.对数据库操作
            this.update()
                    .setSql("liked = liked - 1").eq("id", id).update();
            return Result.ok();
        }
        //没点过赞，则实现点赞功能
        Boolean add = stringRedisTemplate.opsForZSet()
                .add("like:blog:" + String.valueOf(id), String.valueOf(UserHolder.getUser().getId()), LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
        this.update()
                .setSql("liked = liked + 1").eq("id", id).update();
        return Result.ok();
    }

    @Override
    public Result showLikeList(Long id) {
        //显示点赞排行榜，把对这个id的blog的前top5点赞的用户显示，返回List<UserDto>
        //1.根据这个blog去查询redis中这个blog的点赞ZSet
        Set<String> range = stringRedisTemplate
                .opsForZSet().range("like:blog:" + String.valueOf(id), 0, 4);
        //2.查询出点赞用户，并根据它们的点赞时间进行排序存入List<UserDto>中
        List<Long> userIds = range.stream().map(Long::valueOf).collect(Collectors.toList());
        List<User> users = userService.listByIds(userIds);
        List<UserDTO> userDTOS = users.stream().map(item -> {
            UserDTO userDTO = new UserDTO();
            BeanUtil.copyProperties(item, userDTO);
            return userDTO;
        }).collect(Collectors.toList());

        return Result.ok(userDTOS);
    }


}
